package cn.cosmosx.base.calc;

import cn.cosmosx.base.util.ObjectsUtil;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

/**
 * 贷款计算器
 * <pre>
 * <b>一等额本息</b>
 * 本金+利息每月是等额的，即每月还款额是等额的。随着占用资金成本的减少所产生的每月利息也会对应减少，在每月还款额不变的情况下，每月需还款本金是递增的。计算步骤如下：
 * 1.按照推导公式优先计算每月还款额。<code>每月还款额=(贷款本金月利率*(1+月利率)^还款月数))/((1+月利率)^还款月数-1)</code>
 * 2.根据剩余本金计算当期利息，利息=剩余本金*月利率。
 * 3.计算当期本金，本金=每月还款额-利息。
 * 4.最后一期使用倒推法先计算本金，本金=贷款金额-前期合计本金。
 * 5.计算最后一期利息，当期利息=每月还款额-本金。
 * <p>
 * <b>二等额本金</b>
 * 本金每月是等额的，随着占用资金成本的减少所产生的每月利息也会对应减少，在本金不变的情况下，每月还款额是逐月递减的。计算步骤如下：
 * 1.按照推导公式优先计算每月本金， <code>本金=贷款金额/贷款期数</code>。
 * 2.根据剩余本金计算当期利息，利息=剩余本金*月利率。
 * 3.计算每月还款额，每月还款额=本金+利息。
 * 4.最后一期使用倒推法先计算本金，本金=贷款金额-前期合计本金。
 * 5.计算最后一期利息，利息=剩余本金*月利率。
 * 6.计算最后一期还款额，每月还款额=本金+利息。
 * </pre>
 */
public class LoanCalculator {

    /**
     * 等额本息计算公式
     *
     * @param param - 贷款信息
     */
    public static Map<Integer, Loan> calcEqCapitalAndInterest(Param param) {
        Map<Integer, Loan> result = new TreeMap<>(Comparator.comparingInt(o -> o));
        // 总贷款期限
        int loanPeriods = param.getLoanPeriods();
        // 贷款月利率
        BigDecimal monthRate = param.getMonthRate();
        // 每月还款金额
        BigDecimal monthAmount = calMonthRepayment(param);
        // 剩余本金
        BigDecimal balance = param.getLoanAmount();
        // 出最后一期本金和
        BigDecimal noneLastCapitalTotal = BigDecimal.ZERO;
        Loan loan;
        LocalDate date = ObjectsUtil.isNull(param.getStartRepaymentDate()) ? LocalDate.now() : param.getStartRepaymentDate();// 还款日
        for (int i = 1; i <= loanPeriods; i++) {
            loan = new Loan().setPeriod(i).setAmount(monthAmount);// 每月还款金额
            loan.setInterest(calMonthInterest(balance, monthRate));// 利息=剩余本金*月利率
            loan.setCapital(monthAmount.subtract(loan.getInterest()));// 本金=每月还款额-利息
            loan.setCapitalBalance(balance = balance.subtract(loan.getCapital()));// 当期剩余本金
            loan.setRepaymentDate(date.plusMonths(i - 1));// 还款日
            result.put(i, loan);
            if (i != loanPeriods) {
                noneLastCapitalTotal = noneLastCapitalTotal.add(loan.getCapital());
            }
        }
        // 修正最后一期的数值
        Loan lastLoan = result.get(loanPeriods);
        lastLoan.setCapital(param.getLoanAmount().subtract(noneLastCapitalTotal));// 最后一期使用倒推法先计算本金，本金=贷款金额-前期合计本金。
        lastLoan.setInterest(monthAmount.subtract(lastLoan.getCapital()));// 计算最后一期利息，当期利息=每月还款额-本金。
        lastLoan.setCapitalBalance(BigDecimal.ZERO);// 最后一期剩余本金为零，剩余的全部加在最后一期本金计算利息
        return result;
    }

    /**
     * 等额本金计算公式
     *
     * @param param - 贷款信息
     */
    public static Map<Integer, Loan> calcEqCapital(Param param) {
        Map<Integer, Loan> result = new TreeMap<>(Comparator.comparingInt(o -> o));
        // 总贷款期限
        int loanPeriods = param.getLoanPeriods();
        // 贷款月利率
        BigDecimal monthRate = param.getMonthRate();
        // 每月还款本金
        BigDecimal monthCapital = calMonthCapital(param);
        // 剩余本金
        BigDecimal balance = param.getLoanAmount();
        Loan loan;
        LocalDate date = ObjectsUtil.isNull(param.getStartRepaymentDate()) ? LocalDate.now() : param.getStartRepaymentDate();// 还款日
        for (int i = 1; i <= loanPeriods; i++) {
            loan = new Loan().setPeriod(i).setCapital(monthCapital);// 本金
            loan.setInterest(calMonthInterest(balance, monthRate));// 利息=剩余本金*月利率
            loan.setAmount(loan.getCapital().add(loan.getInterest()));// 每月还款额=本金+利息
            loan.setCapitalBalance(balance = balance.subtract(monthCapital));// 当期剩余本金
            loan.setRepaymentDate(date.plusMonths(i - 1));// 还款日
            result.put(i, loan);
        }
        // 修正最后一期的数值
        Loan lastLoan = result.get(loanPeriods);
        lastLoan.setCapital(param.getLoanAmount().subtract(monthCapital.multiply(BigDecimal.valueOf(loanPeriods - 1))));// 最后一期使用倒推法先计算本金，本金=贷款金额-前期合计本金。
        lastLoan.setInterest(calMonthInterest(lastLoan.getCapital(), monthRate));// 计算最后一期利息，利息=剩余本金*月利率。
        lastLoan.setAmount(lastLoan.getCapital().add(lastLoan.getInterest()));// 计算最后一期还款额，每月还款额=本金+利息。
        lastLoan.setCapitalBalance(BigDecimal.ZERO);// 最后一期剩余本金为零，剩余的全部加在最后一期本金计算利息
        return result;
    }

    /**
     * 计算等额本息还款月供
     *
     * @param param 贷款信息包含贷款金额、年利率、期限
     * @return 等额本息还款月供
     */
    private static BigDecimal calMonthRepayment(Param param) {
        BigDecimal totalRate = param.getMonthRate().add(BigDecimal.ONE);
        return fromMetadata(param.getLoanAmount().multiply((param.getMonthRate().multiply(totalRate.pow(param.getLoanPeriods()))).divide(totalRate.pow(param.getLoanPeriods()).subtract(BigDecimal.ONE), RoundingMode.HALF_EVEN)), 2);
    }

    /**
     * 计算等额本金每月还款本金
     *
     * @param param 贷款信息包含贷款金额、年利率
     * @return 等额本金每月还款本金
     */
    private static BigDecimal calMonthCapital(Param param) {
        return fromMetadata(param.getLoanAmount().divide(new BigDecimal(param.getLoanPeriods()), 2, RoundingMode.HALF_EVEN), 2);
    }

    /**
     * 计算利息
     *
     * @param restCapital 剩余本金
     * @param monthRate   月利率
     * @return 当期利息
     */
    private static BigDecimal calMonthInterest(BigDecimal restCapital, BigDecimal monthRate) {
        return fromMetadata(restCapital.multiply(monthRate), 2);
    }

    /**
     * 银行家算法截取指定位数
     *
     * @param bigDecimal 待格式化数据
     * @param digit      截取位数
     */
    static BigDecimal fromMetadata(BigDecimal bigDecimal, int digit) {
        return bigDecimal.setScale(digit, RoundingMode.HALF_EVEN);
    }
}
